﻿namespace Bowlus.AppHost
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    using System.Security.AccessControl;
    using System.Threading;

    using Bowlus.Common;

    using Microsoft.Web.Administration;
    using Microsoft.WindowsAzure.ServiceRuntime;

    // Maintain the List of Applications Deployed to this Instance
    public class IISAppInfo
    {
        // i.e. 'test1.bowlus.net'
        public string HostHeader { get; set; }

        public string CurrentPath { get; set; }

        public DateTime Updated { get; set; }
    }
    
    // AppHost Manager
    public class AppHostManager
    {
        private readonly StorageAppRepository appRepository;

        // local root path for all sites
        private string rootSitePath;

        private string appCachePath;

        // provision a site with Staging Status == "provisioning"
        private bool provisionSite(TenantDeployment td)
        {
            // Unzip the site to the host folder
            var sitePath = Path.Combine(rootSitePath, td.HostName, DateTime.UtcNow.Ticks.ToString());
            appRepository.WriteAppTo(td.Application, sitePath);

            // provision in IIS config
            var siteName = RoleEnvironment.CurrentRoleInstance.Id + "_" + td.HostName;
                //<sites>
                //    <site name="Bowlus.AppHost_IN_0_test1.thecloudsamurai.com" id="1">
                //        <application path="/" applicationPool="test1">
                //            <virtualDirectory path="/" physicalPath="C:\Resources\directory\37950156f46746a48bf597b89b5e832d.Bowlus.AppHost.Sites\test1.thecloudsamurai.com\634690683032291523\Site" />
                //        </application>
                //        <bindings>
                //            <binding protocol="http" bindingInformation="10.115.48.42:8080:test1.thecloudsamurai.com" />
                //        </bindings>
                //    </site>
                //</sites>
            var iisApp = new IISAppInfo();
            iisApp.HostHeader = td.HostName;
            // contents in zip file is under 'Site' directory
            iisApp.CurrentPath = Path.Combine(sitePath, "Site");
            iisApp.Updated = new FileInfo(sitePath).LastWriteTimeUtc;

            // try 3 times
            for (int i = 0; ; i++)
            {
                try
                {
                    // add site to IIS config
                    lock (BowlusHelper.ServerManagerLock)
                    {
                        var sm = BowlusHelper.ServerManager;
                        if (sm.Sites.Count(s => s.Name == siteName) > 0)
                        {
                            Trace.TraceWarning("Site already exists in IIS, however we still update and re-provision it." + siteName);
                            var site = sm.Sites.SingleOrDefault(s => s.Name == (siteName));
                            var hostHeaderParts = iisApp.HostHeader.Split('.');
                            var appPoolName = hostHeaderParts.Length == 3 ? hostHeaderParts[0] : iisApp.HostHeader;
                            site.Applications.First().ApplicationPoolName = GetAppPool(sm, appPoolName).Name;
                        }
                        else
                        {
                            var newSite = sm.Sites.Add(siteName, "http",
                                RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["Http"].IPEndpoint + ":" + iisApp.HostHeader, iisApp.CurrentPath);
                            var hostHeaderParts = iisApp.HostHeader.Split('.');
                            var appPoolName = hostHeaderParts.Length == 3 ? hostHeaderParts[0] : iisApp.HostHeader;
                            newSite.Applications.First().ApplicationPoolName = GetAppPool(sm, appPoolName).Name;
                        }
                        sm.CommitChanges();
                    }
                    break;
                }
                catch (Exception ex)
                {
                    Trace.TraceError(ex.Message + '\n' + ex.StackTrace);
                    if (i == 3) throw;
                    Thread.Sleep(3000);
                }
            }

            // update td status
            td.StagingId = RoleEnvironment.CurrentRoleInstance.Id;
            td.SStatus = TenantDeployment.S_PROVISIONED;
            TentantDepTableContext.Instance.UpdateTD(td);

            Trace.TraceInformation("Site provisioned: " + siteName);

            return true;
        }

        // deprovision a site with Staging Status == "deprovisioning"
        private bool deprovisionSite(TenantDeployment td)
        {
            // delete from IIS config
            var siteName = RoleEnvironment.CurrentRoleInstance.Id + "_" + td.HostName;
            if (BowlusHelper.ServerManager.Sites.Count(s => s.Name == siteName) == 0)
            {
                Trace.TraceWarning("Cannot deprovision from IIS a site does not exist. " + siteName);
            }
            else
            {
                // try 3 times
                for (int i = 0; ; i++)
                {
                    try
                    {
                        // real logic, to be tried
                        lock (BowlusHelper.ServerManagerLock)
                        {
                            var sm = BowlusHelper.ServerManager;
                            var site = sm.Sites.SingleOrDefault(s => s.Name == (siteName));
                            var appPoolName = site.Applications[0].ApplicationPoolName;
                            var appPool = sm.ApplicationPools.SingleOrDefault(ap => ap.Name == appPoolName);
                            sm.ApplicationPools.Remove(appPool);
                            sm.Sites.Remove(site);
                            sm.CommitChanges();
                        }
                        break;
                    }
                    catch (Exception ex)
                    {
                        Trace.TraceError(ex.Message + '\n' + ex.StackTrace);
                        if (i == 3) throw;
                        Thread.Sleep(3000);
                    }
                }
            }

            // delete from IIS server file directory
            var directory = Directory.EnumerateDirectories(rootSitePath).SingleOrDefault(d => d.Contains(td.HostName));
            Directory.Delete(directory, true);

            // delete from tenant deployment table
            TentantDepTableContext.Instance.DeleteTD(td);

            Trace.TraceInformation("Site deprovisioned: " + siteName);
            return true;
        }

        // sync can be safely called any times and at anytime.
        public int Sync()
        {
            var ctx = TentantDepTableContext.Instance;
            string localIpv4Address = BowlusHelper.GetHostIpAddress();
            int changes = 0;

            var q = (from td in ctx.TDQueryable 
                     where td.SStatus == TenantDeployment.S_PROVISIONING && td.Staging == localIpv4Address
                     select td).ToList<TenantDeployment>();
            foreach (var td in q)
            {
                try
                {
                    if (provisionSite(td)) changes++;
                }
                catch (Exception ex)
                {
                    Trace.TraceError("Provision fail: "+ td.HostName + ", " + ex.Message);
                    if (RoleEnvironment.IsEmulated)
                        throw;
                }
            }

            q = (from td in ctx.TDQueryable
                 // where td.SStatus == TenantDeployment.S_DEPROVISIONING && td.Staging == localIpv4Address 
                 where td.SStatus == TenantDeployment.S_DEPROVISIONING && td.InstanceId == RoleEnvironment.CurrentRoleInstance.Id
                 select td).ToList<TenantDeployment>();
            foreach (var td in q)
            {
                try
                {
                    if (deprovisionSite(td)) changes++;
                }
                catch (Exception ex)
                {
                    Trace.TraceError("Deprovision fail: " + td.HostName + ", " + ex.Message);
                    if (RoleEnvironment.IsEmulated)
                        throw;
                }
            }

            return changes;
        }

        // get, or create if not exist, application pool for the site
        private static ApplicationPool GetAppPool(ServerManager sm, string siteName)
        {
            var appPool = sm.ApplicationPools.SingleOrDefault(p => p.Name == siteName);
            if (appPool == null)
            {
                appPool = sm.ApplicationPools.Add(siteName);
                appPool.ManagedRuntimeVersion = "v4.0";
                appPool.ProcessModel.LoadUserProfile = false;
            }
            return appPool;
        }

        // Configure the directory security.
        private static void ConfigureDirectorySecurity(string rootPath, string path)
        {
            var sec = Directory.GetAccessControl(rootPath);
            sec.AddAccessRule(new FileSystemAccessRule("Everyone", FileSystemRights.FullControl, InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, PropagationFlags.None, AccessControlType.Allow));
            Directory.SetAccessControl(path, sec);
        }

        // set appRepository and serverManager
        public AppHostManager(StorageAppRepository repository)
        {
            appRepository = repository;
        }

        // 1) init sitesPath and appCachePath
        // 2) provision active sites
        // init will be called at a later time than AppHostManager constructor, when the VM is ready.
        public void Initialize()
        {
            Trace.TraceInformation("Initialize AppHostManager");

            try
            {
                // Initialize Local Resources
                this.rootSitePath = RoleEnvironment.GetLocalResource("Sites").RootPath.TrimEnd('\\');
                this.appCachePath = RoleEnvironment.GetLocalResource("AppCache").RootPath.TrimEnd('\\');

                // Configure Security on our directories
                ConfigureDirectorySecurity(this.rootSitePath, this.rootSitePath);
                ConfigureDirectorySecurity(this.rootSitePath, this.appCachePath);

                // try to provision active site.
                // this will only happen when the VM starts, while the TenantDeployment table 'Active' sites
                // are not in VM. Because the deployment table persists while VMs don't.
                
                var ctx = TentantDepTableContext.Instance;
                var q = (from td in ctx.TDQueryable 
                         where td.Status == TenantDeployment.S_ACTIVE && td.InstanceId == RoleEnvironment.CurrentRoleInstance.Id
                         select td).ToList<TenantDeployment>();
                foreach (var td in q)
                {
                    provisionSite(td);
                }

                Sync();
            }
            catch (Exception ex)
            {
                Trace.TraceError(ex.Message + '\n' + ex.StackTrace);
                throw;
            }
        }
    }
}